#!/usr/bin/env python
# -*- coding: utf-8 -*-
import redis
import json
from flask import current_app, has_app_context
from common.settings.constants import UTF8_ENCODING

class RedisClient:
    _instance = None

    def __new__(cls, *args, **kwargs):
        if not cls._instance:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self):
        self.client = None

    def init_app(self, app):
        # 如果已经初始化过 Redis 客户端，直接返回，避免重复初始化
        if self.client:
            return

        # 从 Flask 配置里读取 Redis 连接参数，创建 StrictRedis 客户端实例
        self.client = redis.StrictRedis(
            host=app.config.get('REDIS_HOST'),  # Redis 服务器地址
            port=app.config.get('REDIS_PORT'),  # Redis 端口号
            db=app.config.get('REDIS_DB'),  # 选择 Redis 数据库编号
            password=app.config.get('REDIS_PASSWORD'),  # 连接密码
            decode_responses=True  # 返回字符串而非字节类型
        )

        try:
            # 测试连接，向 Redis 发送 PING 命令，确认连通性
            self.client.ping()
            app.logger.info('Redis 连接成功 ✅')
        except redis.ConnectionError as e:
            # 连接失败时，捕获异常并打印错误日志
            app.logger.error(f'Redis 连接失败 ❌: {e}')

    def get_client(self):
        # 获取 Redis 客户端实例
        if not self.client:
            # 如果客户端未初始化，尝试自动初始化
            if has_app_context():
                # 当前有 Flask 应用上下文，自动用 current_app 初始化 Redis 客户端
                self.init_app(current_app)
            else:
                # 没有 Flask 上下文，无法自动初始化，记录错误并抛异常阻止继续执行
                current_app.logger.error('RedisClient 未初始化，且无 Flask 上下文，无法自动绑定')
                raise RuntimeError("RedisClient 未初始化，且无 Flask 上下文，无法自动绑定")

        # 返回已经初始化好的 Redis 客户端
        return self.client

    def set(self, name, value, is_json=False, time=None):
        """
        设置键值对
        :param name:
        :param value:
        :param is_json: 值是否为JSON格式
        :param time:
        :return:
        """
        try:
            if is_json:
                serialized_value = json.dumps(value).encode(UTF8_ENCODING)
            else:
                serialized_value = str(value).encode(UTF8_ENCODING)
        except (TypeError, ValueError, UnicodeEncodeError) as e:
            raise ValueError(f"无法序列化值: {value}") from e

        try:
            if time:
                self.get_client().setex(name=name, value=serialized_value, time=time)
            else:
                self.get_client().set(name=name, value=serialized_value)
        except Exception as e:
            raise RuntimeError(f"Redis [key：{name}]设置失败: {e}") from e

    def get(self, name):
        """
        获取键对应的值
        :param name:
        :return:
        """
        client = self.get_client()
        try:
            raw_value = client.get(name=name)
            return raw_value
        except (ConnectionError, TimeoutError, UnicodeDecodeError) as e:
            raise RuntimeError(f'Redis get 失败，key: {name}，错误: {e}')

    def set_hash(self, name, key, value, is_json=False, time=None):
        """
        设置哈希表中的键值对，一条数据中多个键值对
        :param name:
        :param key:
        :param value:
        :param is_json: 值是否为JSON格式
        :param time:
        :return:
        """
        if not isinstance(name, (str, bytes)) or not isinstance(key, (str, bytes)):
            raise ValueError("name 和 key 必须是字符串或字节类型")

        client = self.get_client()
        if is_json:
            try:
                value = json.dumps(value, ensure_ascii=False, indent=2)
            except (TypeError, OverflowError) as e:
                raise ValueError(f"JSON 序列化失败: {e}")

        try:
            # 使用 pipeline 确保 hset 和 expire 的原子性
            with client.pipeline() as pipe:
                pipe.hset(name, key, value)
                if time:
                    pipe.expire(name, time)
                pipe.execute()
        except Exception as e:
            raise RuntimeError(f"[Redis Error] set_hash 执行失败: {e}")

    def get_hash(self, name, key):
        """
        获取哈希表中的键值对
        :param name:
        :param key:
        :return:
        """
        try:
            value = self.get_client().hget(name, key)
            if value:
                if isinstance(value, str):
                    return value
                elif isinstance(value, bytes):
                    return value
                else:
                    # 根据业务需求决定如何处理非预期类型
                    return str(value)
            else:
                return None
        except Exception as e:
            raise RuntimeError(f"Redis 获取哈希表数据发生异常: {e}")

    def exists(self, name):
        """
        判断键是否存在
        :param name:
        :return: bool
        """
        if not isinstance(name, (str, bytes)):
            current_app.logger.warning(f"输入的key类型无效: {type(name)}")
            return False
        try:
            return self.get_client().exists(name)
        except Exception as e:
            raise RuntimeError(f"Redis 检查键【{name}】是否存在发生未知异常：{str(e)}")

    def delete(self, *keys):
        """
        删除指定的Redis键：delete('key1')、delete('key1', 'key2')
        :param keys:
        :return:
        """
        try:
            return self.get_client().delete(*keys)
        except Exception as e:
            current_app.logger.error(f'Redis 删除发生未知异常: {e}，keys: {keys}')
            return 0

    def keys(self, pattern):
        """
        获取所有符合条件的键
        :param pattern:
        :return: list
        """
        if not isinstance(pattern, str) or not pattern:
            return []

        client = self.get_client()
        try:
            # 使用 SCAN 替代 KEYS，避免阻塞 Redis
            return list(client.scan_iter(match=pattern))
        except Exception as e:
            current_app.logger.warning(f"Redis scan_iter failed: {e}")
            return []

    def ttl(self, name):
        """获取键的过期时间"""
        try:
            ttl_seconds = self.get_client().ttl(name)
        except Exception as e:
            raise RuntimeError(f"Redis 获取键过期时间发生异常，键名： '{name}': {e}")

        if ttl_seconds == -1:
            return None  # 键存在但没有设置过期时间
        elif ttl_seconds == -2:
            return None  # 键不存在
        else:
            return ttl_seconds  # 键存在且有设置过期时间，返回剩余过期时间（秒）


# 单例实例，业务代码直接用它就行
redis_client = RedisClient()

# 供 init_plugs.py 调用的初始化函数
def init_redis(app):
    redis_client.init_app(app)
